import MediaInfoFactory from 'mediainfo.js';
import { Buffer } from 'buffer';
import { createMD5 } from "hash-wasm";
import { PDFDocument } from 'pdf-lib';
import { ZipReader, BlobReader, TextWriter, BlobWriter } from '@zip.js/zip.js';

class FileBuffer {
    constructor(file, buffer) {
        this._file = file;
        this._buffer = buffer;
    }

    get buffer() {
        return this._buffer;
    }

    getBlob() {
        return new Blob([this._buffer], { type: this._file.type });
    }

    async unzip(fn) {
        const zipFileReader = new BlobReader(this.getBlob());
        const zipReader = new ZipReader(zipFileReader);
        const entries = (await zipReader.getEntries()).map(a => new ZipEntry(a));
        const result = await Promise.resolve().then(() => fn && fn(entries));
        await zipReader.close();
        return result;
    }
}

class ZipEntry {
    constructor(entry) {
        this._entry = entry;
    }

    get entry() {
        return this._entry;
    }

    get filename() {
        return this._entry.filename;
    }

    async getText() {
        return this._entry.getData(new TextWriter());
    }

    async getBlob() {
        return this._entry.getData(new BlobWriter());
    }
}

class MetadataGetter {
    key() { }

    isMatch(type) { }

    async ready(file) { }

    chunk(chunk) { }

    async get(fileBuffer) { }
}

class FileMetadataGetter extends MetadataGetter {
    constructor() {
        super();
        this._data = null;
    }

    key() {
        return "base";
    }

    isMatch() {
        return true;
    }

    ready(file) {
        this._data = {
            type: file.type,
            name: file.name
        };
    }

    get() {
        return this._data;
    }
}

class MediaMetadataGetter extends MetadataGetter {
    constructor() {
        super();
    }

    key() {
        return "media";
    }

    isMatch(type) {
        let [a] = type.split('/');
        return a === 'video' || a === 'audio';
    }

    ready(file) {
        this._data = {
            type: file.type,
            name: file.name
        };
    }

    async get(fileBuffer) {
        const mediainfo = await MediaInfoFactory();
        return mediainfo.analyzeData(() => fileBuffer.buffer.byteLength, () => fileBuffer.buffer);
    }
}

class DocMetadataGetter extends MetadataGetter {
    key() {
        return 'doc';
    }

    isMatch(type) {
        return type.indexOf('vnd.openxmlformats-officedocument.wordprocessingml.document') != -1;
    }

    async get(fileBuffer) {
        return await fileBuffer.unzip(async entries => {
            let target = entries.find(a => a.filename === 'docProps/app.xml');
            if (target) {
                let content = await target.getText();
                const doc = new DOMParser().parseFromString(content, "application/xml");
                return {
                    pages: doc.querySelector('Pages').innerHTML,
                    words: doc.querySelector('Words').innerHTML
                };
            }
        });
    }
}

class PptMetadataGetter extends MetadataGetter {
    key() {
        return 'ppt';
    }

    isMatch(type) {
        return type.indexOf('vnd.openxmlformats-officedocument.presentationml.presentation') != -1;
    }

    async get(fileBuffer) {
        return await fileBuffer.unzip(async entries => {
            let target = entries.find(a => a.filename === 'docProps/app.xml');
            if (target) {
                let content = await target.getText();
                const doc = new DOMParser().parseFromString(content, "application/xml");
                return {
                    pages: doc.querySelector('Slides').innerHTML,
                    words: doc.querySelector('Words').innerHTML
                };
            }
        });
    }
}

class PdfMetadataGetter extends MetadataGetter {
    key() {
        return 'pdf';
    }

    isMatch(type) {
        return type.indexOf('pdf') != -1;
    }

    async get(fileBuffer) {
        const t = await PDFDocument.load(fileBuffer.buffer);
        return t.getPageCount();
    }
}

class HashMetadataGetter extends MetadataGetter {
    constructor() {
        super();
        this._md5 = null;
    }

    key() {
        return "hash";
    }

    isMatch(type) {
        return true;
    }

    async ready() {
        const md5 = this._md5 = await createMD5();
        md5.init();
    }

    chunk(chunk) {
        this._md5.update(chunk);
    }

    async get() {
        return this._md5.digest();
    }
}

const getters = [
    FileMetadataGetter,
    MediaMetadataGetter,
    HashMetadataGetter,
    DocMetadataGetter,
    PptMetadataGetter,
    PdfMetadataGetter,
]

const utils = {
    fetch(stream, fn) {
        return new Promise(resolve => {
            const reader = stream.getReader();
            let r = [];
            function pump() {
                return reader.read().then(({ done, value }) => {
                    if (done) {
                        resolve(Buffer.concat(r));
                        return;
                    }
                    r.push(value);
                    fn(value);
                    return pump();
                });
            }
            pump();
        });
    }
}

export const FilesMetadataGetter = {
    register(getterClass) {
        getters.push(getterClass);
    },
    async getMetadata(file) {
        let list = getters.map(a => new a()).filter(a => a.isMatch(file.type));
        await Promise.all(list.map(a => a.ready(file)));
        let buffer = await utils.fetch(file.stream(), chunk => {
            list.map(a => a.chunk(chunk));
        });
        let r = {}, fileBuffer = new FileBuffer(file, buffer);
        await Promise.all(list.map(a => {
            return Promise.resolve().then(() => a.get(fileBuffer)).then(data => {
                r[a.key()] = data;
            });
        }));
        return r;
    }
}